--[[
    定时器, 支持间隔定时器和时间定时器

    间隔定时器: 每 interval(单位: 秒) 时间后执行一次 func
    每间隔 interval 时间执行一次 func，wait 为第一次执行等待时间，immediate 为是否立即执行一次
    interval 可以是函数，也可以是数字，如果是函数，它应该返回下次执行的时间戳
    interval(func, interval, wait, immediate)

    -- 每隔指定时间(min, hour, day)执行一次 func，wait 为第一次执行等待时间，immediate 为是否立即执行一次
    intervalSec(func, sec, wait, immediate)    -- 每隔 sec 秒执行一次 func
    intervalMin(func, min, wait, immediate)
    intervalHour(func, hour, wait, immediate)
    intervalDay(func, day, wait, immediate)
    intervalWeek(func, week, wait, immediate)
    
    -- 每隔指定时间(min, hour, day)执行一次 func, immediate 为是否立即执行一次
    -- atStart 表示是否在对应时间点开始的时候执行，如果为 true 则等待到对应时间(min, hour, day)开始的时候执行, 否则从当前时间开始计时
    everySec(func, immediate)
    everyMin(func, atStart, immediate)
    everyHour(func, atStart, immediate)
    everyDay(func, atStart, immediate)

    时间定时器: 当时间到达 time_tbl 时执行
    -- 当时间到达 time_tbl 时执行 func, immediate 为是否立即执行一次
    when(func, time_tbl, immediate)
    whenMin(func, min, immediate)
    whenHour(func, hour, min, immediate)
    whenDay(func, day, hour, min, immediate)
    whenWday(func, wday, hour, min, immediate)
]] local skynet = require "skynet"
local TimeUtil = require "core.TimeUtil"

local pcall = pcall
local ipairs = ipairs
local table = table
local type = type
local assert = assert

local SECOND = 1
local MINUTE = SECOND * 60
local HOUR = MINUTE * 60
local DAY = HOUR * 24
local WEEK = DAY * 7

local TIMER_INTERVAL = skynet.getenvn("timer_interval") or 100
local M = {
    SECOND = SECOND,
    MINUTE = MINUTE,
    HOUR = HOUR,
    DAY = DAY,
    WEEK = WEEK,
    -- 是否正在运行中
    running = false
}

-- timer 实例列表
local timers = {}

local function start()
    local function startInterval()
        local time = TimeUtil.time()
        for i = 1, #timers do
            local timer = timers[i]
            if time >= timer.next then -- >= 防止出现时间回拨导致的误差
                local ok, err = pcall(timer.func)
                if not ok then
                    LERR("_TIMER:", err)
                elseif err == false then
                    -- timer 不需要再次执行，从 timers 列表中删除，并将 i 减一，以执行下一个
                    table.remove(timers, i)
                    i = i - 1
                end

                -- 计算下次执行时间
                if type(timer.interval) == "function" then
                    timer.next = timer.interval()
                else
                    timer.next = time + timer.interval
                end
            end
        end

        if #timers > 0 then
            -- 反复执行，形成 interval 效果
            skynet.timeout(TIMER_INTERVAL, startInterval)
        else
            -- 停止定时器循环，下次有新定时器时开启
            M.running = false
        end
    end

    M.running = true
    skynet.timeout(TIMER_INTERVAL, startInterval)
end

local timer_id = 0
local function newTimer(func, interval, wait)
    ---@class timer
    ---@field func function 回调函数
    ---@field interval number|function 每次执行间隔时间
    ---@field next number 下次执行时间
    local timer = {
        id = timer_id,
        func = func,
        interval = interval
    }

    if wait then
        timer.next = TimeUtil.time() + wait
    elseif type(interval) == "function" then
        -- 计算下次执行时间
        timer.next = interval()
    else
        timer.next = TimeUtil.time() + interval
    end

    timer_id = timer_id + 1
    table.insert(timers, timer)

    if not M.running then
        -- 启动定时器
        start()
    end

    return timer
end

-- 关闭定时器
function M.close(timerId)
    for i = 1, #timers, 1 do
        if timers[i].id == timerId then
            table.remove(timers, i)
            return
        end
    end
end

function M.clear()
    timers = {}
    timers.running = false
end

-- 间隔定时器，每 interval(100 为 1 秒) 时间后执行一次 func
---@param func function: 执行函数，返回 false 则退出定时器
---@param interval number: 时间间隔
---@param wait number|nil: 第一次执行等待时间，默认为 false，不等待
---@param immediate boolean|nil: 是否立即执行一次，默认为 false
function M.interval(func, interval, wait, immediate)
    assert(type(func) == "function", "func must be a function")
    assert(type(interval) == "number" or type(interval) == "function", "interval must be a number or function")
    if type(wait) == "boolean" then
        immediate = wait
        wait = nil
    end

    if immediate then
        -- 立即执行一次,如果返回 false 则直接退出 interval
        if func() == false then
            return
        end
    end

    return newTimer(func, interval, wait)
end

function M.intervalSec(func, sec, wait, immediate)
    return M.interval(func, sec, wait, immediate)
end

function M.intervalMin(func, min, wait, immediate)
    return M.interval(func, min * MINUTE, wait, immediate)
end

function M.intervalHour(func, hour, wait, immediate)
    return M.interval(func, hour * HOUR, wait, immediate)
end

function M.intervalDay(func, day, wait, immediate)
    return M.interval(func, day * DAY, wait, immediate)
end

function M.intervalWeek(func, week, wait, immediate)
    return M.interval(func, week * WEEK, wait, immediate)
end

-- 每秒执行一次,immediate决定是否立即执行一次
function M.everySec(func, immediate)
    return M.interval(func, SECOND, immediate)
end

-- 每分钟执行一次, wait 为 true 则在每分钟开始时执行，否则，从当前时间开始每一分钟执行
function M.everyMin(func, atStart, immediate)
    return M.interval(func, MINUTE, atStart and TimeUtil.untilMin(1, 's') * SECOND, immediate)
end

function M.everyHour(func, atStart, immediate)
    return M.interval(func, HOUR, atStart and TimeUtil.untilHour(1, 's') * SECOND, immediate)
end

function M.everyDay(func, atStart, immediate)
    return M.interval(func, DAY, atStart and TimeUtil.untilDay(1, 's') * SECOND, immediate)
end

-- 计算下一次执行的时间
local function calNextTime(timeTbl)
    local nextDate = TimeUtil.date("*t")
    nextDate.sec = 0

    if timeTbl.wday then
        nextDate.day = nextDate.day + ((timeTbl.wday - nextDate.wday + 7) % 7)
        nextDate.hour = timeTbl.hour or 0
        nextDate.min = timeTbl.min or 0
    else
        nextDate.min = timeTbl.min or 0
        if timeTbl.month then
            nextDate.month = timeTbl.month
            nextDate.day = timeTbl.day or 1
            nextDate.hour = timeTbl.hour or 0
        elseif timeTbl.day then
            nextDate.day = timeTbl.day
            nextDate.hour = timeTbl.hour or 0
        elseif timeTbl.hour then
            nextDate.hour = timeTbl.hour
        end
    end

    -- 下一次执行时间早于当前时间，则增加相应的时间单位
    local now = TimeUtil.now()
    if TimeUtil.diff(nextDate, now) <= 0 then
        if timeTbl.wday then
            nextDate.day = nextDate.day + 7
        elseif timeTbl.month then
            nextDate.year = nextDate.year + 1
        elseif timeTbl.day then
            nextDate.month = nextDate.month + 1
        elseif timeTbl.hour then
            nextDate.day = nextDate.day + 1
        else
            nextDate.hour = nextDate.hour + 1
        end
    end

    return TimeUtil.time(nextDate)
end

--[[
    当时间到达 time_tbl 时执行
    @params func function: 执行函数，返回 false 则退出定时器
    @params time_tbl number|table: 时间配置，如果为 number，则表示分钟数，如果为 table，则表示时间配置
    @params immediate boolean|nil: 是否立即执行一次，默认为 false
]]
function M.when(func, timeTbl, immediate)
    local wait
    if not immediate then
        wait = calNextTime(timeTbl)
    end

    -- 创建定时器
    return M.interval(func, function()
        return calNextTime(timeTbl)
    end, wait, immediate)
end

-- 当分钟为 min 时，执行
function M.whenMin(func, min, immediate)
    local timeTbl = min
    if type(min) == "number" then
        timeTbl = {
            min = min
        }
    end

    return M.when(func, timeTbl, immediate)
end

-- 当小时为 hour 时执行(可以同时指定分钟: min)
function M.whenHour(func, hour, min, immediate)
    if type(hour) == "table" then
        assert(hour.hour, "whenHour must have hour field")
        return M.when(func, {
            hour = hour.hour,
            min = hour.min or 0
        }, min)
    end

    assert(type(hour) == "number", "hour must be number or table")
    if not min or type(min) == "boolean" then
        immediate = min
        min = 0
    end

    return M.when(func, {
        hour = hour,
        min = min
    }, immediate)
end

-- 当日期为 day 时执行(可以同时指定小时: hour, 分钟: min)
function M.whenDay(func, day, hour, min, immediate)
    assert(type(day) == "table" or type(day) == "number", "day must be a table or number")
    if type(day) == "table" then
        assert(day.day, "whenDay must have day field")
        return M.when(func, {
            day = day.day,
            hour = day.hour or 0,
            min = day.min or 0
        }, hour)
    end

    assert(type(day) == "number", "day must be number or table")
    if not hour or type(hour) == "boolean" then
        immediate = hour
        hour = 0
        min = 0
    elseif not min or type(min) == "boolean" then
        immediate = min
        min = 0
    end

    return M.when(func, {
        day = day,
        hour = hour,
        min = min
    }, immediate)
end

-- 当星期几为 wday 时执行(可以同时指定小时: hour, 分钟: min)
function M.whenWday(func, wday, hour, min, immediate)
    if type(wday) == "table" then
        assert(wday.wday, "whenWday must have wday field")
        return M.when(func, {
            wday = wday.wday,
            hour = wday.hour or 0,
            min = wday.min or 0
        }, hour)
    end

    assert(type(wday) == "number", "wday must be number or table")
    if not hour or type(hour) == "boolean" then
        immediate = hour
        hour = 0
        min = 0
    elseif not min or type(min) == "boolean" then
        immediate = min
        min = 0
    end

    return M.when(func, {
        wday = wday,
        hour = hour,
        min = min
    }, immediate)
end

return M
